package com.kit.lib;

import java.lang.reflect.UndeclaredThrowableException;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

/**
 * 
 * 原始公式:TOTP =Truncate(HMACSHA(K, T);<br/>
 * 现有公式:TOTP =Mix(Truncate(HMACSHA((CODE+SECRET_KEY),(Now-T0-N*STEP)/STEP)),RK);
 * <br/>
 * <br/>
 * 解释：<br/>
 * T=(Now-T0-N*STEP)/STEP) ：是步长，表示当前时间-初始时间后除去步长单位，N为容忍步长个数。<br/>
 * K=(CODE+SECRET_KEY) : 密钥K为自有密钥code和公共密钥SECRET_KEY拼接，code可以是任意约定。<br/>
 * RK : 密钥加盐随机数，用于创建不重复的totp;<br/>
 * HMACSHA ：一种基础信息摘要算法，加密方式可以是：HmacSHA1，HmacSHA256，HmacSHA512 .. <br/>
 * Truncate : 一种截断函数，用于截取TOTP_BASIC_LENGTH位长度totp; <br/>
 * Mix : 一种字符混淆函数，用户将随机数RK插入到以生成的totp中。<br/>
 * <br/>
 * 专利：<br/>
 * step1:,一种防止重复TOTP密码重复的方案；RAND_DIGITS<br/>
 * step2:,一种在时钟不严格同步的情况下TOTP优化；N=EXTEND_STEP<br/>
 * 
 * @author yuanqiyong@unovo.com.cn
 * @date 2019年7月3日
 * @company http://www.lianyuplus.com/
 */
public class TOTP {
	public static void main(String[] args) {
		// 门锁密钥[每个门锁的密钥都不一样]
		String secret_key = "lianyu@12345678901234567890";
		// 生成TOTP
		String totp = buildTOTP(secret_key, new Date());
		System.out.println("生成TOTP:" + totp);
		// 校验TOTP
		// boolean bo = verifyTOTP(secret_key, "11912306", new Date());
		boolean bo = verifyTOTP(secret_key, totp, new Date());
		System.out.println("校验结果：" + (bo ? "成功" : "失败"));

	}

	// 【关键】一个步长单位(精确到秒)：60秒
	private static final long STEP = 60;
	// 【关键】容忍扩展步长10次: def=10正负x2
	private static final long EXTEND_STEP = 10;
	// 【关键】basic长度：def=5
	private static final int TOTP_BASIC_LENGTH = 5;
	// 【关键】TOTP长度：def=8
	private static final int TOTP_LENGTH = 8;

	private TOTP() {
	}

	/**
	 * 使用该方法：生成TOTP
	 * 
	 * @param secret_key 门锁密钥
	 * @param time       开门时间
	 * @return totp密码
	 */
	public static String buildTOTP(String secret_key, Date time) {
		String totp = buildTotpBasic(secret_key, time.getTime() / 1000);// 创建基础位
		// System.err.println("KEY:" + secret_key + "\ntotp_basic:" + totp);
		return Mix3(totp);
	}

	/**
	 * 使用该方法：验证TOTP
	 * 
	 * @param secret_key 门锁密钥
	 * @param totp       待校验的密码
	 * @param time       当前时间
	 * @return totp密码
	 */
	public static boolean verifyTOTP(String secret_key, String totp, Date time) {
		// step2:一种在时钟不严格同步的情况下TOTP优化
		isTrue(EXTEND_STEP >= 0, "容忍步长必须>=0");
		for (int n = 0; n < EXTEND_STEP * 2; n++)// 正负延申 EXTEND_STEP * STEP个步长时间
			if (equalsTotp(buildTotpBasic(secret_key, new Date().getTime() / 1000 + EXTEND_STEP * STEP - n * STEP),
					totp))
				return true;
		return false;
	}

	private static boolean equalsTotp(String totp5, String totp8) {
		// step1: 一种防止重复TOTP密码重复的方案
		isTrue(totp5.length() == TOTP_BASIC_LENGTH && totp8.length() == TOTP_LENGTH, "TOTP非法长度");
		char[] c5 = totp5.toCharArray();// 5位
		char[] c8 = totp8.toCharArray();// 8位
		int j = 0;
		for (char c : c8)
			if (j < TOTP_BASIC_LENGTH && c == c5[j])// 必须先判断大小
				j++;// 顺序递增直到结束
		return j == TOTP_BASIC_LENGTH ? true : false;
	}

	// 混淆方法3 = 以总长8为主体, 下标不能重复
	private static String Mix3(String totp5) {
		List<Integer[]> attr = getAttr(TOTP_LENGTH - 1);//
		List<String> totps = split(totp5);
		StringBuilder sb = new StringBuilder();
		int j = 0;
		for (int i = 0; i < TOTP_LENGTH; i++) {// 以总长8为主体
			boolean bo = false;
			for (Integer[] its : attr) {
				if (its[0] == i) {
					sb.append(its[1]);
					bo = true;
					break;// 下标不重复
				}
			}
			if (!bo && j < totps.size())
				sb.append(totps.get(j++));
		}
		return sb.toString();
	}

	/**
	 * 获取随机下标和对应的随机数
	 */
	private static List<Integer[]> getAttr(int maxIndex) {
		int size = TOTP_LENGTH - TOTP_BASIC_LENGTH;
		Integer[] random = getRandom(size, 0, 9, true);// 随机占位数
		Integer[] index = getRandom(size, 0, maxIndex, false);// 随机下标数
		List<Integer[]> attr = new ArrayList<Integer[]>();
		for (int i = 0; i < size; i++)
			attr.add(new Integer[] { index[i], random[i] });
		// System.out.print(JSON.toJSONString(attr));
		return attr;
	}

	private static Integer[] getRandom(int size, int min, int max, boolean bo) {
		Set<Integer> set = new HashSet<Integer>();
		Integer[] rk = new Integer[size];
		for (int i = 0; i < size; i++) {
			rk[i] = (int) Math.round(Math.random() * (min - max) + max);
			if (!bo)// 不许重复
				if (set.contains(rk[i]))
					i--;
				else
					set.add(rk[i]);
		}
		return rk;
	}

	/**
	 * 哈希加密
	 */
	private static byte[] hmac_sha(String crypto, byte[] keyBytes, byte[] text) {
		try {
			Mac hmac = Mac.getInstance(crypto);
			SecretKeySpec macKey = new SecretKeySpec(keyBytes, "AES");
			hmac.init(macKey);
			return hmac.doFinal(text);
		} catch (GeneralSecurityException gse) {
			throw new UndeclaredThrowableException(gse);
		}
	}

	private static String generateTOTP(String key, String time) {
		return generateTOTP(key, time, "HmacSHA1");// HmacSHA256 HmacSHA512
	}

	private static String generateTOTP(String key, String time, String crypto) {
		byte[] msg = time.getBytes();
		byte[] k = key.getBytes();
		byte[] hash = hmac_sha(crypto, k, msg);
		return truncate(hash);
	}

	/**
	 * timeNow精确到秒
	 */
	public static String buildTotpBasic(String code, long timeNow) {
		isTrue(code != null && code.trim().length() > 0, "内容不许为空");
		String time = String.valueOf(timeNow / STEP);
		while (time.length() < 10)
			time = "0" + time;
		// System.err.println("TIME:" + time);
		return generateTOTP(code, time);// 原始
	}

	/**
	 * 截断函数
	 */
	private static String truncate(byte[] target) {
		StringBuilder result;
		int offset = target[target.length - 1] & 0xf;
		int binary = ((target[offset] & 0x7f) << 24) | ((target[offset + 1] & 0xff) << 16)
				| ((target[offset + 2] & 0xff) << 8) | (target[offset + 3] & 0xff);
		int otp = (int) (binary % Math.pow(10, TOTP_BASIC_LENGTH));
		result = new StringBuilder(Integer.toString(otp));
		while (result.length() < TOTP_BASIC_LENGTH)
			result.insert(0, "0");
		return result.toString();
	}

	// 同Assert.isTrue()
	private static void isTrue(boolean bo, String errMsg) {
		if (!bo)
			throw new IllegalArgumentException(errMsg);
	}

	// Java自带的split有问题，在linux 和 win上 长度不一致
	private static List<String> split(String str) {
		List<String> list = new ArrayList<String>();
		for (char i : str.toCharArray())
			list.add(String.valueOf(i));
		return list;
	}
}